All files / src/app/api/collections/[id]/pool/upload route.ts

95% Statements 19/20
80% Branches 8/10
100% Functions 4/4
95% Lines 19/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                    5x 5x 1x   4x   4x 4x 2x   2x             6x 6x 6x 6x         5x 2x                 1x     1x       1x                   3x   3x      
import { NextRequest, NextResponse } from "next/server";
import { handleUpload, type HandleUploadBody } from "@vercel/blob/client";
import { getSession } from "@/lib/auth";
import { getDb } from "@/lib/db";
import { PhotoController } from "@/server/api/photos";
import { requireApiCapability } from "@/server/dal/collection-access";
import { POOL_ALLOWED_CONTENT_TYPES, POOL_MAX_BYTES } from "@/lib/uploads";
import { errorResponse, HttpError } from "@/server/api/http-errors";
 
async function assertPhotosWrite(collectionId: string): Promise<string> {
  const session = await getSession();
  if (!session?.user) {
    throw new HttpError("Unauthorized", 401);
  }
  const db = getDb();
  // photos:write = curator — any curator may upload pool photos (full-curator model).
  const access = await requireApiCapability(db, collectionId, session.user.id, "photos:write");
  if (access instanceof NextResponse) {
    throw new HttpError(access.status === 404 ? "Not found" : "Forbidden", access.status);
  }
  return session.user.id;
}
 
export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  try {
    const { id } = await params;
    const body = (await req.json()) as HandleUploadBody;
    const jsonResponse = await handleUpload({
      body,
      request: req,
      onBeforeGenerateToken: async () => {
        // User request: cookies present → authorize and capture the uploader.
        const uploadedBy = await assertPhotosWrite(id);
        return {
          allowedContentTypes: [...POOL_ALLOWED_CONTENT_TYPES],
          maximumSizeInBytes: POOL_MAX_BYTES,
          addRandomSuffix: true,
          tokenPayload: JSON.stringify({ collectionId: id, uploadedBy }),
        };
      },
      onUploadCompleted: async ({ blob, tokenPayload }) => {
        // Blob callback: NO session. Trust only the signed tokenPayload.
        Iif (!tokenPayload) {
          throw new Error("Missing token payload");
        }
        const { collectionId, uploadedBy } = JSON.parse(tokenPayload) as {
          collectionId: string;
          uploadedBy: string;
        };
        await PhotoController.fromDb(getDb()).recordPooled({
          collectionId,
          uploadedBy,
          blobKey: blob.url,
          filename: blob.pathname,
          mimeType: blob.contentType ?? "application/octet-stream",
          fileSize: null,
        });
      },
    });
    return NextResponse.json(jsonResponse);
  } catch (error) {
    return errorResponse(error);
  }
}